// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2023. All rights reserved.
 * Author: Huawei OS Kernel Lab
 * Create: Fri Sep 23 10:45:12 2022
 */

#include <net/genetlink.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/skbuff.h>
#include <linux/genetlink.h>
#include <liblinux/netlink.h>

static atomic_t nl_pal_inited = ATOMIC_INIT(0);
static struct netlink_pal_handler nl_handlers;

int liblinux_pal_netlink_init(struct netlink_pal_handler *hdr)
{
	if (atomic_cmpxchg(&nl_pal_inited, 0, 1) != 0)
		return -EEXIST;

	nl_handlers = *hdr;
	return 0;
}
EXPORT_SYMBOL(liblinux_pal_netlink_init);

int liblinux_netlink_pal_unicast(struct sk_buff *skb, int proto, unsigned int dst_portid)
{
	int ret = 0;

	if (nl_handlers.unicast_hdr != NULL)
		ret = nl_handlers.unicast_hdr(skb->data, skb->len,
					      proto, dst_portid);

	if (ret < 0)
		return ret;

	return 0;
}

int liblinux_netlink_pal_multicast(struct sk_buff *skb, int proto, unsigned int dst_grp)
{
	int ret = 0;

	if (nl_handlers.multicast_hdr != NULL)
		ret = nl_handlers.multicast_hdr(skb->data, skb->len,
						proto, dst_grp);

	if (ret < 0)
		return ret;

	return 0;
}

int liblinux_netlink_genl_pal_reg(const char *name, const void *fam_info, unsigned long info_len,
				  unsigned int *fam_id, unsigned int *mcgroup_offset)
{
	if (nl_handlers.genl_reg_hdr != NULL)
		return nl_handlers.genl_reg_hdr(name, fam_info, info_len, fam_id, mcgroup_offset);

	return -ENOSYS;
}

int liblinux_netlink_genl_pal_unreg(unsigned int fam_id)
{
	if (nl_handlers.genl_unreg_hdr != NULL)
		return nl_handlers.genl_unreg_hdr(fam_id);

	return -ENOSYS;
}

int liblinux_netlink_rtnl_pal_reg(const char *name)
{
	struct liblinux_nl_info nl_info;

	if (nl_handlers.nl_reg_hdr != NULL) {
		nl_info.type = RT_NETLINK;
		nl_info.rtnl.name = name;
		return nl_handlers.nl_reg_hdr(&nl_info);
	}
	return -ENOSYS;
}

int liblinux_netlink_rtnl_pal_unreg(const char *name)
{
	struct liblinux_nl_info nl_info;

	if (nl_handlers.nl_unreg_hdr != NULL) {
		nl_info.type = RT_NETLINK;
		nl_info.rtnl.name = name;
		return nl_handlers.nl_unreg_hdr(&nl_info);
	}
	return -ENOSYS;
}

struct genl_reg_info {
	char name[GENL_NAMSIZ];
	unsigned int n_mcgroups;
	struct genl_multicast_group group_names[0];
};
static void *__build_reg_genl_info(const struct genl_family *family, unsigned long *genl_info_len)
{
	size_t info_sz = sizeof(struct genl_reg_info);
	size_t group_names_sz = sizeof(struct genl_multicast_group) * family->n_mcgrps;

	struct genl_reg_info *fam_info = (struct genl_reg_info *)
					 kmalloc(info_sz + group_names_sz, GFP_KERNEL);
	if (fam_info == NULL)
		return NULL;

	memcpy(fam_info->name, family->name, sizeof(family->name));
	fam_info->n_mcgroups = family->n_mcgrps;
	memcpy(fam_info->group_names, family->mcgrps, group_names_sz);
	*genl_info_len = info_sz + group_names_sz;
	return fam_info;
}

int __pre_alloc_genl_family_res(struct genl_family *family)
{
	int err;
	unsigned int grp_start = 0;
	unsigned int fam_id_new = family->id;
	void *fam_reg_info = NULL;
	unsigned long genl_info_len = 0;

	if (family->id == GENL_ID_CTRL) /* skip local nlctrl */
		return 0;

	fam_reg_info = __build_reg_genl_info(family, &genl_info_len);
	if (fam_reg_info == NULL)
		return -EFAULT;

	family->mcgrp_offset = 0;
	err = liblinux_netlink_genl_pal_reg(family->name, fam_reg_info, genl_info_len,
					    &fam_id_new, &grp_start);
	kfree(fam_reg_info);
	if (err < 0)
		return err;

	family->id = fam_id_new;
	family->mcgrp_offset = grp_start;
	return 0;
}

void __free_pre_res_of_genl_family(const struct genl_family *family)
{
	if (family->id == GENL_ID_CTRL)
		return;

	liblinux_netlink_genl_pal_unreg(family->id);
}

static bool __protocol_need_register(unsigned int protocol)
{
	if (protocol == NETLINK_KOBJECT_UEVENT || protocol == NETLINK_GENERIC ||
	    protocol == NETLINK_ROUTE)
		return false;

	return true;
}

int netlink_pal_proto_register(unsigned int unit, unsigned int nr_grp)
{
	struct liblinux_nl_info nl = {
		.type = CUST_NETLINK,
		.cust_nl = {
			.unit = unit,
			.nr_grp = nr_grp,
		},
	};

	if (!__protocol_need_register(unit))
		return 0;

	if (nl_handlers.nl_reg_hdr != NULL)
		return nl_handlers.nl_reg_hdr(&nl);

	return -ENOSYS;
}

int netlink_pal_proto_unregister(unsigned int unit)
{
	struct liblinux_nl_info nl = {
		.type = CUST_NETLINK,
		.cust_nl = {
			.unit = unit,
			.nr_grp = 0,
		},
	};

	if (!__protocol_need_register(unit))
		return 0;

	if (nl_handlers.nl_unreg_hdr != NULL)
		return nl_handlers.nl_unreg_hdr(&nl);

	return -ENOSYS;
}
